-
-
Notifications
You must be signed in to change notification settings - Fork 763
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add search for a trusted host in ProxyHeadersMiddleware #591
Conversation
I found a discussion about this feature: 723d3dc#r36079732 So, I'll try to find examples from other servers with this feature and make comparison |
else: | ||
self.trusted_hosts = trusted_hosts | ||
self.trusted_hosts = set(trusted_hosts) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For speedup str not in collection
operations
x-forwarded-for
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From my understanding, the idea here is to return the actual client ip address. This as it is returns the last ip address (which could be a proxy address)
if self.always_trust: | ||
return x_forwarded_for_hosts[0] | ||
|
||
for host in reversed(x_forwarded_for_hosts): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's just a security feature: we should return only the last non-trusted host.
Trivial case:
- Proxy-server A
- Proxy-server B
- Client C
- We trust both of proxies (
trusted_hosts = A, B
) or trust every proxy (trusted_hosts = *
)
So in this scenario, a header will be X-Forwarded-For: C, B, A
and we can securely extract C as a trusted client address.
Unfortunately, IRL we have a situation when we have a chain of proxies, and we can trust only a few of them, e.g:
- Proxy-server A
- Proxy-server B
- Client C
- We trust only A (
trusted_hosts = A
)
In this case, non-trusted proxy-server B can replace the real client address, so we should return the first non-trusted host and it will be B
. Because the chain is actually reverted, we should check it also with reversed
.
tl;dr: In a simple case, we can trust all of the proxies with trusted_hosts = *
and it will return real client address (the first in X-Forwarded-For)
if self.always_trust: | ||
return x_forwarded_for_hosts[0] | ||
|
||
for host in reversed(x_forwarded_for_hosts): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's just a security feature: we should return only the last non-trusted host.
Trivial case:
- Proxy-server A
- Proxy-server B
- Client C
- We trust both of proxies (
trusted_hosts = A, B
) or trust every proxy (trusted_hosts = *
)
So in this scenario, a header will be X-Forwarded-For: C, B, A
and we can securely extract C as a trusted client address.
Unfortunately, IRL we have a situation when we have a chain of proxies, and we can trust only a few of them, e.g:
- Proxy-server A
- Proxy-server B
- Client C
- We trust only A (
trusted_hosts = A
)
In this case, non-trusted proxy-server B can replace the real client address, so we should return the first non-trusted host and it will be B
. Because the chain is actually reverted, we should check it also with reversed
.
tl;dr: In a simple case, we can trust all of the proxies with trusted_hosts = *
and it will return real client address (the first in X-Forwarded-For)
Ok, I think I get it now. Thanks |
This change is working well for me. The old way of specifying N proxies worked better for my use case because google cloud load balancers + kubernetes services always adds two hosts to the header, and I don't get to know what those are in advance. I can get this to work with your changes. Thank you! |
@tomchristie please can someone take a look at this PR? Sorry for the inconvenience 🙏 |
Is there any chance to release this any time soon? |
|
||
for host in reversed(x_forwarded_for_hosts): | ||
if host not in self.trusted_hosts: | ||
return host |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need a fall through case so there's no way we can end up returning None
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I fail to see how it would be possible
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would happen if all the IPs in the chain were explicitly listed in trusted_hosts
- but this would only occur if trusted_hosts
were incorrectly set and/or the first reverse proxy in the chain failed to set x-forwarded-for
to the client's IP (not our fault).
In this case, is there any other justifiable return value than None
? I can't think of one
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm, this would be a great addition, is there something we can do @tomchristie to alleviate your concerns on the above point ?
@euri10 I'm not sure a fall-through case is necessary above - @tomchristie could you take another look at this PR please? I've just been bitten by this problem in the wild and it would be good to get this merged. Thanks |
updated it in https://github.com/encode/uvicorn/compare/master...euri10:b0g3r-uvicorn-590?expand=1 to solve conflicts because of the "new" test suite (original PR was done previous to the test suite being "revamped") |
…ed-for` Also rewrite a few tests for increasing coverage for edge cases
@euri10 Thank you for your comment! I rebased this PR onto master and fix related issues ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just correct this small typo in the comment and we gtg I think, this is a great addition thanks @b0g3r
Co-authored-by: euri10 <[email protected]>
x_forwarded_for_hosts = [ | ||
item.strip() for item in x_forwarded_for.split(",") | ||
] | ||
host = self.get_trusted_client_host(x_forwarded_for_hosts) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what if the x-forwarded-for format is IP1:PORT1,IP2:PORT2,which is common in the L7 load balance? the port here is always set zero which is not reasonable.
* Fix ProxyHeadersMiddleware, now it grabs the first IP from `x-forwarded-for` Also rewrite a few tests for increasing coverage for edge cases * Add searching algorithm for the trusted client host in x-forwarded-for * Fix issues after rebasing * Fix typo in comment Co-authored-by: euri10 <[email protected]> Co-authored-by: euri10 <[email protected]>
Fixes #590
If we have
*
in trusted proxies we should return the first host in theX-Forwarded-For
as the client address.In other cases, we should iterate over
X-Forwarded-For
hosts backward and return the first host which isn't included in trusted proxies